// -----------------------------------------------------------------------
// <copyright file="CacheManagerInstance.cs" company="Softserve">
// Work with cache
// </copyright>
// -----------------------------------------------------------------------
namespace WebServer.BusinessLogic.Web.Cache
{
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading;
    using WebServer.BusinessLogic.Helpers;
    using WebServer.BusinessLogic.Logs;
using WebServer.BusinessLogic.Handlers.Interface;

    /// <summary>
    /// Class for Cache
    /// </summary>
    public class CacheManagerInstance
    {
        /// <summary>
        /// Clock interface
        /// </summary>
        private IClock myClock;

        /// <summary>
        /// Cache lock
        /// </summary>
        private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();

        /// <summary>
        /// Caches items
        /// </summary>
        private Dictionary<string, CachedItem> cachedItems = new Dictionary<string, CachedItem>();

        /// <summary>
        /// sync root
        /// </summary>
        private object syncRoot = new object();

        private Configurator _configurator;
        private IFileSystemProvider _fileSystemProvider;

        /// <summary>
        /// Initializes a new instance of the <see cref="CacheManagerInstance" /> class.
        /// </summary>
        public CacheManagerInstance()
        {
            this.myClock = new SystemClock();
            this._configurator = Configurator.Instance;
            this._fileSystemProvider = new FileSystemProvider();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="CacheManagerInstance" /> class.
        /// Constructor for mocking clock
        /// </summary>
        /// <param name="clock">interface for mocking</param>
        public CacheManagerInstance(IClock clock) : this()
        {
            this.myClock = clock;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="CacheManagerInstance" /> class.
        /// Constructor for mocking clock
        /// </summary>
        public CacheManagerInstance(Configurator configurator)
            : this()
        {
            this._configurator = configurator;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="CacheManagerInstance" /> class.
        /// Constructor for mocking fileSystemProvider
        /// </summary>
        /// <param name="fileSystemProvider">interface for mocking</param>
        public CacheManagerInstance(IFileSystemProvider fileSystemProvider)
            : this()
        {
            this._fileSystemProvider = fileSystemProvider;
        }

        /// <summary>
        /// Gets max cache size
        /// </summary>
        public int MaxCacheSize
        {
            get { return _configurator.MaxCacheSize; }
        }

        /// <summary>
        /// Gets total size
        /// </summary>
        public long TotalSize
        {
            get
            {
                this.cacheLock.EnterReadLock();
                try
                {
                    return this.cachedItems.Sum(i => i.Value.DataSize);
                }
                finally
                {
                    this.cacheLock.ExitReadLock();
                }
            }
        }

        /// <summary>
        /// Gets item from cache
        /// </summary>
        /// <param name="key">Key to lookup</param>
        /// <returns>Null when not found, CachedItem on success</returns>
        public CachedItem GetCachedItem(string key)
        {
            this.cacheLock.EnterUpgradeableReadLock();
            try
            {
                // get data from list
                if (_configurator.IsCacheEnabled && this.cachedItems.ContainsKey(key))
                {
                    this.cacheLock.EnterWriteLock();
                    try
                    {
                        // increase number of times this key was requested
                        this.cachedItems[key].AccessedTimes++;

                        // save last access time for further cleanup
                        this.cachedItems[key].LastAccessTime = this.myClock.Now;
                    }
                    finally
                    {
                        this.cacheLock.ExitWriteLock();
                    }

                    // log
                    Logger.Instance.Log(string.Format("Reading Cache '{0}'", key));

                    this.cacheLock.EnterReadLock();
                    try
                    {
                        return this.cachedItems[key];
                    }
                    finally
                    {
                        this.cacheLock.ExitReadLock();
                    }
                }

                // on failure, return null
                return null;
            }
            finally
            {
                this.cacheLock.ExitUpgradeableReadLock();
            }
        }

        /// <summary>
        /// Adds or updates existing key of cache
        /// </summary>
        /// <param name="key">Key of the cache</param>
        /// <param name="value">Data to store in it</param>
        public void AddItem(string key, byte[] value)
        {
            this.cacheLock.EnterUpgradeableReadLock();
            try
            {
                long requestedSize = value.Length;
                bool isCacheFree = true;

                // check if we're not out of memory
                if (this.TotalSize + requestedSize > this.MaxCacheSize)
                {
                    // out of memory, remove less usable items
                    isCacheFree = this.FreeCacheToFitLength(requestedSize);
                }

                if (isCacheFree)
                {
                    if (this.cachedItems.ContainsKey(key))
                    {
                        // get referenced variable from cache
                        CachedItem ci = this.cachedItems[key];

                        // update data field
                        ci.Data = value;
                    }
                    else
                    {
                        // create new key when cache is empty
                        CachedItem ci = new CachedItem(value);

                        // save creation time
                        ci.CreationTime = this.myClock.Now;

                        this.cacheLock.EnterWriteLock();
                        try
                        {
                            // add item to cache
                            this.cachedItems.Add(key, ci);
                        }
                        finally
                        {
                            this.cacheLock.ExitWriteLock();
                        }

                        // log
                        string logLine = string.Format(
                            "Cache for key '{0}' created with size {1} bytes. Free: {2:0.##}MB",
                            key,
                            ci.DataSize,
                            (this.MaxCacheSize - this.TotalSize) / 1024 / 1024);
                        Logger.Instance.Log(logLine, true);
                    }
                }
            }
            finally
            {
                this.cacheLock.ExitUpgradeableReadLock();
            }
        }

        /// <summary>
        /// Removing item by key
        /// </summary>
        /// <param name="key">string key</param>
        public void RemoveItemByKey(string key)
        {
            this.cacheLock.EnterWriteLock();
            try
            {
                // add item to cache
                this.cachedItems.Remove(key);
            }
            finally
            {
                this.cacheLock.ExitWriteLock();
            }
        }

        /// <summary>
        /// Check requested size for being larger then maximum size of cache
        /// </summary>
        /// <param name="requestedSize">size of request</param>
        /// <returns>is request size larger cache size</returns>
        private bool FreeCacheToFitLength(long requestedSize)
        {
            //// requested size can't be larger then maximum size of cache
            try
            {
                this.cacheLock.EnterReadLock();
                if (requestedSize > this.MaxCacheSize || this.cachedItems.Count == 0)
                {
                    return false;
                }
            }
            finally
            {
                this.cacheLock.ExitReadLock();
            }

            // check for enough space for new item
            long totalSize = this.TotalSize;
            while (totalSize + requestedSize > this.MaxCacheSize)
            {
                KeyValuePair<string, CachedItem> notUsedItem;
                try
                {
                    this.cacheLock.EnterReadLock();
                    int minimalAccessTimes = this.cachedItems.Min(i => i.Value.AccessedTimes);
                    notUsedItem = this.cachedItems.First(c => c.Value.AccessedTimes == minimalAccessTimes);
                }
                finally
                {
                    this.cacheLock.ExitReadLock();
                }

                if (notUsedItem.Value != null)
                {
                    // change total size of collection
                    totalSize -= notUsedItem.Value.DataSize;

                    try
                    {
                        this.cacheLock.EnterWriteLock();

                        // delete less usable item
                        this.cachedItems.Remove(notUsedItem.Key);
                    }
                    finally
                    {
                        this.cacheLock.ExitWriteLock();
                    }
                }
            }

            return true;
        }

        /// <summary>
        /// Getting data from file or cache
        /// </summary>
        /// <param name="cacheKey">key of cache</param>
        /// <param name="filePath">path to file</param>
        /// <returns>byte array of data</returns>
        public byte[] GetCacheOrFile(string cacheKey, string filePath)
        {
            byte[] data;
            bool saveToCache = false;

            if (_configurator.IsCacheEnabled)
            {
                var cacheItem = this.GetCachedItem(cacheKey);
                if (cacheItem != null)
                {
                    data = cacheItem.Data;
                    return data;
                }

                saveToCache = true;
            }

            data = _fileSystemProvider.ReadBytesFromFile(filePath);
            if (saveToCache)
            {
                this.AddItem(cacheKey, data);
            }

            return data;
        }    
    }
}